module(..., package.seeall)

local Versium = {}


----------------------------------------------------------------------------
-- Instantiates a new versium object.
-- 
-- @param impl_mod   the name implementation module.
-- @param params     the parameters to pass to the implementation.
-- @return           a new versium object.
----------------------------------------------------------------------------

function Versium.new(self, args)
   local obj = {}
   setmetatable(obj, self)
   self.__index = self

   local storage_mod = require(args.storage or "versium.storage.simple")
   obj.storage = storage_mod.open(args.params, obj)

   local inflator_mod = require(args.inflator or "versium.inflators.lua")
   obj.inflator = inflator_mod.make_inflator(args.params, obj)

   return obj 
end


----------------------------------------------------------------------------
-- Returns a table representing the node with a given id.
--
-- @param version    the desired version of the node (defaults to latest).
-- @return           the node as a string.
----------------------------------------------------------------------------

function Versium.get_node(self, id, version)
   local node = self.storage:get_node(self:escape_id(id), version)
   node.id = id
   node.version = version
   return node
end


----------------------------------------------------------------------------
-- Returns true if the node exists and false otherwise.
-- 
-- @param id         an id of an node.
-- @return           true or false.
----------------------------------------------------------------------------

function Versium.node_exists (self, id)
   return self.storage:node_exists(self:escape_id(id))
end


----------------------------------------------------------------------------
-- Returns a table with the metadata for the latest version of the
-- node, same as get_history(id)[1] but potentially cheaper.  If the
-- node doesn't exist, "nil" is returned.
-- 
-- @param id         an id of an node.
-- @return           the metadata for the latest version or nil.
----------------------------------------------------------------------------

function Versium.get_node_info (self, id)
   return self.storage:node_info(self:escape_id(id))
end


----------------------------------------------------------------------------
-- Lists ids for all existing nodes
-- 
-- @return           a list of IDs
----------------------------------------------------------------------------

function Versium.get_node_ids (self)
   return self.storage:get_node_ids()
end


----------------------------------------------------------------------------
-- Saves a new version of the node.
--
-- @param id         the id of the node.
-- @param data       the value to save.
-- @param author     the user name to be associated with the change.
-- @param comment    the change comment.
-- @param extra      any extra metadata
-- @return           the version of ID of the new node.
----------------------------------------------------------------------------

function Versium.save_version(self, id, data, author, comment, extra)
   return self.storage:save_version(self:escape_id(id), data, 
                                 author, comment, extra)
end


----------------------------------------------------------------------------
-- Return the history of the node as a list of tables, each
-- representing a revision of the node (with fields like version,
-- author, comment, and extra).  The version can be filtered by a time
-- prefix.
--
-- @param id         the id of the node.
-- @param prefix     time prefix.
-- @return           a list of tables representing the versions.
----------------------------------------------------------------------------

function Versium.get_node_history (self, id, prefix)
   return self.storage:get_node_history(self:escape_id(id), prefix)
end

----------------------------------------------------------------------------
-- Inflate an node, turning it into a Lua table.
--
-- @param node       A table representing the node's metadata, with the 
--                   string value of the node in .data.
-- @return           A table representing the fields of the node, with 
--                   the metadata and the string representatoin pushed 
--                   into the metatable.
----------------------------------------------------------------------------

function Versium.inflate(self, node, param)
   --local object = self:load_lua(node.data, {})
   local object = self.inflator:inflate(node.data, param)

   local meta = {
      _version = {
         id        = node.version,
         timestamp = node.timestamp,
         author    = node.author,
         comment   = node.comment,
         extra     = node.extra,
      },
      _raw = node.data
   }
   meta.__index = meta
   setmetatable(object, meta)
   return object
end


----------------------------------------------------------------------------
-- An auxiliary function to split a string into tokens for diffing.
--
--  @param text          A string to be split.
--  @return              A list of tokens.
----------------------------------------------------------------------------

local function _diffsplit(text) 
   local l = {}
   local text = text .. " "
   for token, sep in string.gmatch(text, "([^%s]*)(%s)") do
      l[#l+1] = token
      l[#l+1] = sep
   end
   return l
end


----------------------------------------------------------------------------
-- An auxiliary coroutine to diff strings.  The function returns a
-- list of pairs, where the first value represents a token and the
-- second the token's status ("same", "in", "out").
--
-- @param t1         The "old" text string
-- @param t1         The "new" text string
-- @return           A list of annotated tokens.
----------------------------------------------------------------------------

function diff(t1, t2)
   local t2 = _diffsplit(t2)
   local t1 = _diffsplit(t1)
   local l = {}
   local diff = {
      put = function(self, token, type) 
               table.insert(self, {token,type})
            end,
      IN = "in",
      OUT = "out",
      SAME = "same"
   }

   require("lcs")
   local subseq = lcs.longestCommonSubseq(t1, t2, l)
   local is = 1
   local i1 = 0
   local i2 = 1
   
   while true do
      i1 = i1 + 1
      
      if i1 > #t1 then                                  -- done with t1 
         for j=i2,#t2 do
            diff:put(t2[j], diff.IN)
         end
         for j=is,#subseq do
            diff:put(subseq[j], diff.SAME)
         end
         break                                          -- done
      end

      if t1[i1] == subseq[is] then 
         while true do   ---- fastforward through T2 ---
            if t2[i2] == t1[i1] or i2 > #t2 then 
               break 
            else
               if t2[i2] then 
                  diff:put(t2[i2], diff.IN)
               end
               i2 = i2 + 1
            end
         end
         i2 = i2 + 1
         if subseq[is] then 
            diff:put(subseq[is], diff.SAME)
         end
         is = is + 1
      else 
         diff:put(t1[i1], diff.OUT)
      end
   end
   return diff
end

----------------------------------------------------------------------------
-- Diff the raw string representation of two revisions of the node.
--
-- @id               the node id.
-- @param v1         the "old" revision id.
-- @param v2         the "new" revision id.
-- @return           a list of annotated tokens.
----------------------------------------------------------------------------

function Versium.diff(self, id, v1, v2)
   return diff(self:get_node(id, v1).data, self:get_node(id, v2).data)
end


----------------------------------------------------------------------------
-- Diff inflated representations of two revisions of the node,
-- field-by-field.
--
-- @id               the node id.
-- @param v1         the "old" revision id.
-- @param v2         the "new" revision id.
-- @return           a table of lists of annotated tokens, keyed by field 
--                   name.
----------------------------------------------------------------------------

function Versium.smart_diff(self, id, v1, v2)
   local node1 = self:inflate(self:get_node(id, v1))
   local node2 = self:inflate(self:get_node(id, v2))
   local difftab  = {}
   for field, value in pairs(node1) do
      if node1[field] ~= node2[field] then
         difftab[field] = diff(tostring(node1[field]), tostring(node2[field]))
      end
   end
   return difftab
end


----------------------------------------------------------------------------
-- Quotes an arbitrary string with enough ==='s.
--
-- @param text       the id of the node.
-- @return           long-quoted text.
----------------------------------------------------------------------------

function Versium.longquote(self, text)

   local max_eqs = ""
   string.gsub(text, "%[(=*)%[",
               function(match)
                  if max_eqs:len() < match:len() then
                     max_eqs = match
                  end
               end
            )
   local eqs = max_eqs.."="
   return string.format("[%s[%s]%s]", eqs, text, eqs)
end


----------------------------------------------------------------------------
-- Escapes a node id to make it safe for use as a file name.
-- 
-- @param id         a node id.
-- @return           an escaped node id.
----------------------------------------------------------------------------

function Versium.escape_id(self, id) 
   return string.gsub(id, ":", "%3A")
end


----------------------------------------------------------------------------
-- Un-escapes a node id. (See escape_id().)
-- 
-- @param id         a escaped node id.
-- @return           the original node id.
----------------------------------------------------------------------------

function Versium.unescape_id(self, id) 
   return string.gsub(id, "%3A", ":")
end


----------------------------------------------------------------------------
-- Loads a chunk of Lua code safely, returning a table of variables
-- defined by the code.
--  
-- @param text       the text to be loaded as Lua code.
-- @param env        the table containing the environment in which
--                   the code will be run.
-- @return           a table representing the variables defined by the code.
----------------------------------------------------------------------------

function Versium.load_lua (self, text, environment)
   if text then 
      f,e = loadstring(text)
      if e then 
         return {}, "can't parse lua code in a page:\n"
            .. e .. "\nLua code: " .. text
      end
   end
   if f then 
      setfenv(f, environment)
      x = f()
      results = {}
      for k, v in pairs(getfenv(f)) do
         results[k] = v
      end
      return results 
   end
   return nil
end


----------------------------------------------------------------------------
-- Instantiate a new versium object.
-- 
-- @param impl       the implementation module.
-- @param params     the parameters to pass to the implementation.
-- @return           a new versium object.
----------------------------------------------------------------------------

function open(args)
   return Versium:new(args)
end